package com.apinji.unionpay.service.serviceimpl;

import com.apinji.common.domain.OpenCardLog;
import com.apinji.common.domain.OpenCardSmsLog;
import com.apinji.common.domain.UserCardToken;
import com.apinji.common.domain.mapper.OpenCardLogMapper;
import com.apinji.common.domain.mapper.OpenCardSmsLogMapper;
import com.apinji.common.domain.mapper.UserCardTokenMapper;
import com.apinji.common.domain.wrapper.Reply;
import com.apinji.common.plugin.DateUtils;
import com.apinji.common.plugin.Mutual;
import com.apinji.common.plugin.StrUtils;
import com.apinji.common.plugin.UnionpayUtils;
import com.apinji.common.plugin.fastjson.JSONUtils;
import com.apinji.unionpay.common.BankCardTypeEnum;
import com.apinji.unionpay.common.UnionpayErrorCodeEnum;
import com.apinji.unionpay.dto.*;
import com.apinji.unionpay.protocol.AlipayCardInfoResponse;
import com.apinji.unionpay.service.BankCardService;
import com.apinji.unionpay.sdk.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * Created by 轩辕夏夏 on 2017/7/1.
 */
@Service
public class BankCardServiceImpl implements BankCardService {

    @Autowired
    private UserCardTokenMapper userCardTokenMapper;

    @Autowired
    private OpenCardSmsLogMapper openCardSmsLogMapper;

    @Autowired
    private OpenCardLogMapper openCardLogMapper;

    @Override
    public Reply getInUsedCards(String userUUID) {
        if(StrUtils.isEmpty(userUUID)) {
            return Mutual.invalidParam("用户UUID为空！");
        }
        //
        List<UserCardToken> resList = userCardTokenMapper.getInUsedCards(userUUID);
        //
        return Mutual.success(resList);
    }

    @Override
    public Reply isOpen(QueryIsOpenDto queryIsOpenDto) {
        UserCardToken userCardTokenDb = userCardTokenMapper.getOneByCardNo(queryIsOpenDto.getUserUUID(), queryIsOpenDto.getCardNo());
        if(null == userCardTokenDb) {
            return Mutual.success(null, false);
        }
        //
        return Mutual.success(userCardTokenDb, true);
    }

    @Override
    public Reply delUserCard(DelUsedCardDto delUsedCardDto) {
        UserCardToken userCardToken = userCardTokenMapper.getOneById(delUsedCardDto.getId(), delUsedCardDto.getUserUUID());
        if(null == userCardToken || StrUtils.isEmpty(userCardToken.getId())) {
            return Mutual.notExist("不存在该卡号的token信息");
        }
        userCardTokenMapper.delById(delUsedCardDto.getId(), delUsedCardDto.getUserUUID());
        return Mutual.success();
    }

    //@Override
    public Reply openCardSMSBackToken(OpenCardSMSBackTokenDto openCardSMSBackTokenDto) {
        Map<String, String> contentData = new HashMap<String, String>();

        /***银联全渠道系统，产品参数，除了encoding自行选择外其他不需修改***/
        contentData.put("version", SDKConfig.getConfig().getVersion());                  //版本号
        contentData.put("encoding", "UTF-8");                   //字符集编码 可以使用UTF-8,GBK两种方式
        contentData.put("signMethod", SDKConfig.getConfig().getSignMethod()); //签名方法
        contentData.put("txnType", "77");                              //交易类型 77-发送短信
        contentData.put("txnSubType", "00");                           //交易子类型 00-开通短信
        contentData.put("bizType", "000902");                          //业务类型 无跳转
        contentData.put("channelType", "07");                          //渠道类型07-PC

        /***商户接入参数***/
        contentData.put("merId", SDKConfig.getConfig().getMerId());    //商户号码（本商户号码仅做为测试调通交易使用，该商户号配置了需要对敏感信息加密）测试时请改成自己申请的商户号，【自己注册的测试777开头的商户号不支持代收产品】
        contentData.put("accessType", "0");                            //接入类型，商户接入固定填0，不需修改
        //对外订单号，需要支付系统自己按照规则生成
        String outOrderId = UnionpayUtils.getOutOrderId();
        contentData.put("orderId", outOrderId);             			   //商户订单号，8-40位数字字母，不能含“-”或“_”，可以自行定制规则
        String txnTime = DateUtils.getCurrentDateTime();
        contentData.put("txnTime", txnTime);                             //订单发送时间，格式为YYYYMMDDhhmmss，必须取当前时间，否则会报txnTime无效
        contentData.put("accType", "01");                              //账号类型

        Map<String,String> customerInfoMap = new HashMap<String,String>();
        customerInfoMap.put("phoneNo", openCardSMSBackTokenDto.getPhoneNo());			           //手机号

        ////////////如果商户号开通了【商户对敏感信息加密】的权限那么需要对 accNo,phoneNo加密使用：
        String accNo1 = AcpService.encryptData(openCardSMSBackTokenDto.getCardNo(), "UTF-8");  //这里测试的时候使用的是测试卡号，正式环境请使用真实卡号
        contentData.put("accNo", accNo1);
        contentData.put("encryptCertId",AcpService.getEncryptCertId());       //加密证书的certId，配置在acp_sdk.properties文件 acpsdk.encryptCert.path属性下
        String customerInfoStr = AcpService.getCustomerInfoWithEncrypt(customerInfoMap,null,"UTF-8");

        /////////如果商户号未开通【商户对敏感信息加密】权限那么不需对敏感信息加密使用：
        //contentData.put("accNo", accNo);            		//这里测试的时候使用的是测试卡号，正式环境请使用真实卡号
        //String customerInfoStr = AcpService.getCustomerInfo(customerInfoMap,null);
        ////////

        contentData.put("customerInfo", customerInfoStr);

        //======================
        OpenCardSmsLog openCardSmsLog = new OpenCardSmsLog();
        String id = UUID.randomUUID().toString();
        openCardSmsLog.setId(id);
        openCardSmsLog.setUserUUID(openCardSMSBackTokenDto.getUserUUID());
        openCardSmsLog.setOrderId(openCardSMSBackTokenDto.getOrderId());
        openCardSmsLog.setCardNo(openCardSMSBackTokenDto.getCardNo());
        openCardSmsLog.setPhoneNo(openCardSMSBackTokenDto.getPhoneNo());
        openCardSmsLog.setCreateTime(new Date());
        openCardSmsLog.setVersion(SDKConfig.getConfig().getVersion());
        openCardSmsLog.setMerId(SDKConfig.getConfig().getMerId());
        openCardSmsLog.setOutOrderId(outOrderId);
        openCardSmsLog.setTxnTime(txnTime);
        openCardSmsLog.setAccType("01");
        openCardSmsLog.setStatus("create");
        openCardSmsLog.setMemo("");
        //
        openCardSmsLogMapper.addOneItem(openCardSmsLog);
        //======================

        /**对请求参数进行签名并发送http post请求，接收同步应答报文**/
        Map<String, String> reqData = AcpService.sign(contentData,"UTF-8");			 //报文中certId,signature的值是在signData方法中获取并自动赋值的，只要证书配置正确即可。
        String requestBackUrl = SDKConfig.getConfig().getBackRequestUrl();   				     //交易请求url从配置文件读取对应属性文件acp_sdk.properties中的 acpsdk.backTransUrl
        Map<String, String> rspData = AcpService.post(reqData,requestBackUrl,"UTF-8"); //发送请求报文并接受同步应答（默认连接超时时间30秒，读取返回结果超时时间30秒）;这里调用signData之后，调用submitUrl之前不能对submitFromData中的键值对做任何修改，如果修改会导致验签不通过

        /**对应答码的处理，请根据您的业务逻辑来编写程序,以下应答码处理逻辑仅供参考------------->**/
        //应答码规范参考open.unionpay.com帮助中心 下载  产品接口规范  《平台接入接口规范-第5部分-附录》
        if(!rspData.isEmpty()){
            if(AcpService.validate(rspData, "UTF-8")){
                LogUtil.writeLog("验证签名成功");
                String respCode = rspData.get("respCode") ;
                if(("00").equals(respCode)){
                    openCardSmsLogMapper.updateLogStatus(id, "success", "开通时验证码发送成功");
                    //成功
                    return Mutual.success();
                }else{
                    //其他应答码为失败请排查原因或做失败处理
                    openCardSmsLogMapper.updateLogStatus(id, "failed", "开通时验证码发送失败，银联返回错误码为(" + respCode+")，" + UnionpayErrorCodeEnum.getMsg(respCode));
                    return Mutual.unionpayError(respCode);
                }
            }else{
                LogUtil.writeErrorLog("验证签名失败");
                openCardSmsLogMapper.updateLogStatus(id, "failed", "开通时验证签名失败");
                //TODO 检查验证签名失败的原因
                return Mutual.serverError("验证签名失败");
            }
        }else{
            //未返回正确的http状态
            LogUtil.writeErrorLog("未获取到返回报文或返回http状态码非200");
            openCardSmsLogMapper.updateLogStatus(id, "failed", "未获取到返回报文或返回http状态码非200");
            return Mutual.serverError("未获取到返回报文或返回http状态码非200");
        }
        //String reqMessage = DemoBase.genHtmlResult(reqData);
        //String rspMessage = DemoBase.genHtmlResult(rspData);
        //resp.getWriter().write("请求报文:<br/>"+reqMessage+"<br/>" + "应答报文:</br>"+rspMessage+"");
    }

    @Override
    public Reply openCardBackToken(OpenCardBackTokenDto openCardBackTokenDto) throws IOException {
        //1.先检查该卡号是否已经开通过，如果已经开通过，无需重复开通
        UserCardToken userCardToken = userCardTokenMapper.getOneByCardNo(openCardBackTokenDto.getUserUUID(), openCardBackTokenDto.getCardNo());
        //已经开通
        if(null != userCardToken && !StrUtils.isEmpty(userCardToken.getId())) {
            return Mutual.haveExist("该卡已经开通，无需重复开通");
        }


        int cardType = openCardBackTokenDto.getCardType();

        if(cardType != BankCardTypeEnum.NORMAL_BANK_CARD.getCode() && cardType != BankCardTypeEnum.CREDIT_CARD.getCode()) {
            return Mutual.invalidParam("需要开通的银行卡类型错误！");
        }
        //如果是开通信用卡，以下参数必须传
        //贷记卡 必送：卡号、手机号、CVN2、有效期；验证码看业务配置（默认不要短信验证码）。
        if(cardType == BankCardTypeEnum.CREDIT_CARD.getCode()) {
            if(StrUtils.isEmpty(openCardBackTokenDto.getCvn2())) {
                return Mutual.invalidParam("卡背后三位参数为空！");
            }
            if(StrUtils.isEmpty(openCardBackTokenDto.getExpired())) {
                return Mutual.invalidParam("有效期为空！");
            }
        }


        //发起post请求到银联 开通该卡，返回token信息
        Reply reply = openCardBackTokenPost(openCardBackTokenDto.getUserUUID(), cardType, openCardBackTokenDto.getCardNo(),
                openCardBackTokenDto.getOrderId(), openCardBackTokenDto.getPhoneNo(), openCardBackTokenDto.getCvn2(), openCardBackTokenDto.getExpired(), openCardBackTokenDto.getSmsCode());
        if(!reply.getSuccess()) {
            return reply;
        } else {
            //绑定用户和该卡的关联关系
            userCardTokenMapper.addBankCardToken((UserCardToken)reply.getData());
            return Mutual.success();
        }
    }



    @Override
    public Reply openCardFrontToken(HttpServletRequest request, HttpServletResponse resp, OpenCardFrontTokenDto openCardFrontTokenDto) throws IOException {
        String cardNo = openCardFrontTokenDto.getCardNo();
        String userUUID = openCardFrontTokenDto.getUserUUID();
        String orderId = openCardFrontTokenDto.getOrderId();
        String frontUrl = openCardFrontTokenDto.getFrontUrl();
        //
        return openCardFrontToken(request, resp, userUUID, cardNo, orderId, frontUrl);
    }

    @Override
    public Reply openCardFrontToken(HttpServletRequest request, HttpServletResponse response, String userUUID, String cardNo, String orderId, String frontUrl) throws IOException {
        //1.先判断该卡是否已经绑定到该用户（如果已经绑定，肯定已经开通）
        UserCardToken userCardToken = userCardTokenMapper.getOneByCardNo(userUUID, cardNo);
        //
        if(null != userCardToken && !StrUtils.isEmpty(userCardToken.getId())) {
            LogUtil.writeMessage("该卡["+cardNo+"]已经开通，并且是您的常用卡，无需重复开通");
            return Mutual.haveExist("该卡已经开通，并且是您的常用卡，无需重复开通", userCardToken);
        }

        //2.检查该卡号是否已经开通过，如果已经开通过，无需重复开通    该卡已经开通，但是还未绑定该用户
        //已经开通
//        if(null != bankCardToken && !StrUtils.isEmpty(bankCardToken.getId())) {
//            LogUtil.writeMessage("该卡["+cardNo+"]已经开通，但还不是你的常用卡");
//            return Mutual.haveExist("该卡已经开通，但还不是你的常用卡", bankCardToken);
//        }

        //3.进入开通流程
        response.setContentType("text/html; charset="+ "UTF-8");
        //
        Map<String, String> contentData = new HashMap<String, String>();

        /***银联全渠道系统，产品参数，除了encoding自行选择外其他不需修改***/
        contentData.put("version", SDKConfig.getConfig().getVersion());                  //版本号
        contentData.put("encoding", "UTF-8");            //字符集编码 可以使用UTF-8,GBK两种方式
        contentData.put("signMethod", SDKConfig.getConfig().getSignMethod()); //签名方法
        contentData.put("txnType", "79");                              //交易类型 11-代收
        contentData.put("txnSubType", "00");                           //交易子类型 00-默认开通
        contentData.put("bizType", "000902");                          //业务类型 Token支付
        contentData.put("channelType", "07");                          //渠道类型07-PC

        /***商户接入参数***/
        contentData.put("merId", SDKConfig.getConfig().getMerId());                   			   //商户号码（本商户号码仅做为测试调通交易使用，该商户号配置了需要对敏感信息加密）测试时请改成自己申请的商户号，【自己注册的测试777开头的商户号不支持代收产品】
        contentData.put("accessType", "0");                            //接入类型，商户接入固定填0，不需修改
        String outOrderId = UnionpayUtils.getOutOrderId();
        contentData.put("orderId", outOrderId);             			   //商户订单号，8-40位数字字母，不能含“-”或“_”，可以自行定制规则
        String txnTime = DateUtils.getCurrentDateTime();
        contentData.put("txnTime", txnTime);         				   //订单发送时间，格式为YYYYMMDDhhmmss，必须取当前时间，否则会报txnTime无效
        contentData.put("accType", "01");                              //账号类型

        //测试环境固定trId=62000000001&tokenType=01，生产环境由业务分配。测试环境因为所有商户都使用同一个trId，所以同一个卡获取的token号都相同，任一人发起更新token或者解除token请求都会导致原token号失效，所以之前成功、突然出现3900002报错时请先尝试重新开通一下。
        String trId = SDKConfig.getConfig().getTrId();
        String tokenPayDataStr = "{trId="+trId+"&tokenType=01}";
        contentData.put("tokenPayData", tokenPayDataStr);

        //只支持贷记卡 必送：卡号、手机号、CVN2、有效期；验证码看业务配置（默认不要短信验证码,本测试商户777290058110097配置了需要）。
//		Map<String,String> customerInfoMap = new HashMap<String,String>();
//		customerInfoMap.put("phoneNo", "13552535506");			        //手机号
//		customerInfoMap.put("cvn2", "123");           			        //卡背面的cvn2三位数字
//		customerInfoMap.put("expired", "1711");  				        //有效期 年在前月在后
//		customerInfoMap.put("certifTp", "01");						//证件类型
//		customerInfoMap.put("certifId", "341126197709218366");		//证件号码

        //选送卡号、手机号、证件类型+证件号、姓名
        //也可以都不送,在界面输入这些要素
        //此测试商户号777290058110097 后台开通业务只支持 贷记卡
        //Map<String,String> customerInfoMap = new HashMap<String,String>();
        //customerInfoMap.put("certifTp", "01");						//证件类型
        //customerInfoMap.put("certifId", "341126197709218366");		//证件号码
        //customerInfoMap.put("customerNm", "全渠道");					//姓名
        //customerInfoMap.put("phoneNo", "13552535506");			    //手机号

        ////////////如果商户号开通了【商户对敏感信息加密】的权限那么需要对 accNo，pin和phoneNo，cvn2，expired加密（如果这些上送的话），对敏感信息加密使用：
        String accNo = AcpService.encryptData(cardNo, "UTF-8");  //这里测试的时候使用的是测试卡号，正式环境请使用真实卡号
        contentData.put("accNo", accNo);
        contentData.put("encryptCertId",AcpService.getEncryptCertId());       //加密证书的certId，配置在acp_sdk.properties文件 acpsdk.encryptCert.path属性下
        //String customerInfoStr = AcpService.getCustomerInfoWithEncrypt(customerInfoMap,null,"UTF-8");
        //////////

        /////////如果商户号未开通【商户对敏感信息加密】权限那么不需对敏感信息加密使用：
        //contentData.put("accNo", "6216261000000000018");
        //String customerInfoStr = AcpService.getCustomerInfo(customerInfoMap,null,"UTF-8"_UTF8);   //前台实名认证送支付验证要素 customerInfo中要素不要加密
        ////////
        //contentData.put("customerInfo", customerInfoStr);

        //前台通知地址 （需设置为外网能访问 http https均可），支付成功后的页面 点击“返回商户”的时候将异步通知报文post到该地址
        //如果想要实现过几秒中自动跳转回商户页面权限，需联系银联业务申请开通自动返回商户权限
        //注：如果开通失败的“返回商户”按钮也是触发frontUrl地址，点击时是按照get方法返回的，没有通知数据返回商户
        //String frontUrl = SDKConfig.getConfig().getFrontUrl();
        String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort();
        if(StrUtils.isEmpty(frontUrl)) {
            frontUrl = basePath + "/v1.0/card/openCardFrontFrontRcvResponse";
        }
        contentData.put("frontUrl", frontUrl);

        //后台通知地址（需设置为【外网】能访问 http https均可），支付成功后银联会自动将异步通知报文post到商户上送的该地址，失败的交易银联不会发送后台通知
        //后台通知参数详见open.unionpay.com帮助中心 下载  产品接口规范  网关支付产品接口规范 消费交易 商户通知
        //注意:1.需设置为外网能访问，否则收不到通知    2.http https均可  3.收单后台通知后需要10秒内返回http200或302状态码
        //    4.如果银联通知服务器发送通知后10秒内未收到返回状态码或者应答码非http200，那么银联会间隔一段时间再次发送。总共发送5次，每次的间隔时间为0,1,2,4分钟。
        //    5.后台通知地址如果上送了带有？的参数，例如：http://abc/web?a=b&c=d 在后台通知处理程序验证签名之前需要编写逻辑将这些字段去掉再验签，否则将会验签失败
        String backUrl = basePath + "/v1.0/card/openCardFrontBackRcvResponse";
        contentData.put("backUrl", backUrl);
        LogUtil.writeLog("backUrl--->" + backUrl);

        //contentData.put("backUrl", SDKConfig.getConfig().getBackOpenCardUrl());

        // contentData.put("reserved", "{customPage=true}");         	//如果开通页面需要使用嵌入页面的话，请上送此用法

        // 请求方保留域，
        // 透传字段，查询、通知、对账文件中均会原样出现，如有需要请启用并修改自己希望透传的数据。
        // 出现部分特殊字符时可能影响解析，请按下面建议的方式填写：
        // 1. 如果能确定内容不会出现&={}[]"'等符号时，可以直接填写数据，建议的方法如下。
//		contentData.put("reqReserved", "透传信息1|透传信息2|透传信息3");
        String openCardLogId = StrUtils.uuid();
        contentData.put("reqReserved", openCardLogId);
        // 2. 内容可能出现&={}[]"'符号时：
        // 1) 如果需要对账文件里能显示，可将字符替换成全角＆＝｛｝【】“‘字符（自己写代码，此处不演示）；
        // 2) 如果对账文件没有显示要求，可做一下base64（如下）。
        //    注意控制数据长度，实际传输的数据长度不能超过1024位。
        //    查询、通知等接口解析时使用new String(Base64.decodeBase64(reqReserved), "UTF-8");解base64后再对数据做后续解析。
//		contentData.put("reqReserved", Base64.encodeBase64String("任意格式的信息都可以".toString().getBytes("UTF-8")));

        // 订单超时时间。
        // 超过此时间后，除网银交易外，其他交易银联系统会拒绝受理，提示超时。 跳转银行网银交易如果超时后交易成功，会自动退款，大约5个工作日金额返还到持卡人账户。
        // 此时间建议取支付时的北京时间加15分钟。
        // 超过超时时间调查询接口应答origRespCode不是A6或者00的就可以判断为失败。
        contentData.put("payTimeout", new SimpleDateFormat("yyyyMMddHHmmss").format(new Date().getTime() + 15 * 60 * 1000));


        //============================================================================
        OpenCardLog openCardLog = new OpenCardLog();
        openCardLog.setId(openCardLogId);
        openCardLog.setUserUUID(userUUID);
        openCardLog.setOrderId(orderId);
        //openCardSmsLog.setCardType(cardType);
        //openCardSmsLog.setCvn2(cvn2);
        //openCardSmsLog.setExpired(expired);
        openCardLog.setCardNo(cardNo);
        //openCardSmsLog.setPhoneNo(phoneNo);//回调中有
        openCardLog.setCreateTime(new Date());
        //openCardSmsLog.setSmsCode(smsCode);
        openCardLog.setVersion(SDKConfig.getConfig().getVersion());
        openCardLog.setMerId(SDKConfig.getConfig().getMerId());
        openCardLog.setOutOrderId(outOrderId);
        openCardLog.setTxnTime(txnTime);
        openCardLog.setAccType("01");
        openCardLog.setTokenPayData(tokenPayDataStr);
        openCardLog.setOpenType("frontToken");
        openCardLog.setStatus("create");
        openCardLog.setMemo("");
        openCardLog.setFrontUrl(frontUrl);
        openCardLog.setBackUrl(backUrl);
        //
        openCardLogMapper.addOneItem(openCardLog);
        //============================================================================

        /**请求参数设置完毕，以下对请求参数进行签名并生成html表单，将表单写入浏览器跳转打开银联页面**/
        Map<String, String> reqData = AcpService.sign(contentData,"UTF-8");  			 //报文中certId,signature的值是在signData方法中获取并自动赋值的，只要证书配置正确即可。
        String requestFrontUrl = SDKConfig.getConfig().getFrontRequestUrl();    						 //获取请求银联的前台地址：对应属性文件acp_sdk.properties文件中的acpsdk.frontTransUrl
        String html = AcpService.createAutoFormHtml(requestFrontUrl,reqData,"UTF-8");     //生成自动跳转的Html表单

        LogUtil.writeLog("打印请求HTML，此为请求报文，为联调排查问题的依据："+html);
        response.getWriter().write(html);										    //将生成的html写到浏览器中完成自动跳转打开银联支付页面；这里调用signData之后，将html写到浏览器跳转到银联页面之前均不能对html中的表单项的名称和值进行修改，如果修改会导致验签不通过

        //
        return Mutual.success();
    }

    @Override
    public void openCardFrontBackRcvResponse(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        LogUtil.writeLog("BackRcvResponse接收后台通知开始");

        String encoding = req.getParameter(SDKConstants.param_encoding);
        // 获取银联通知服务器发送的后台通知参数
        Map<String, String> reqParam = UnionpayUtils.getAllRequestParam(req);

        LogUtil.printRequestLog(reqParam);

        Map<String, String> valideData = null;
        if (null != reqParam && !reqParam.isEmpty()) {
            Iterator<Map.Entry<String, String>> it = reqParam.entrySet().iterator();
            valideData = new HashMap<String, String>(reqParam.size());
            while (it.hasNext()) {
                Map.Entry<String, String> e = it.next();
                String key = (String) e.getKey();
                String value = (String) e.getValue();

                valideData.put(key, value);
            }
        }

        //交易成功，更新商户订单状态
        String outOrderId =valideData.get("orderId"); //获取后台通知的数据，其他字段也可用类似方式获取
        //
        if(StrUtils.isEmpty(outOrderId)) {
            LogUtil.writeErrorLog("银联跳转开通卡回调异常，银联回调outOrderId为空");
            return;
        }
        //
        OpenCardLog openCardLog = openCardLogMapper.getOneItemByOutOrderId(outOrderId);
        if(null == openCardLog || StrUtils.isEmpty(openCardLog.getId())) {
            LogUtil.writeErrorLog("银联跳转开通卡回调异常，开通卡日志为空");
            return;
        }
        //
        UserCardToken userCardToken = userCardTokenMapper.getOneByCardNo(openCardLog.getUserUUID(), openCardLog.getCardNo());
        if(null != userCardToken && !StrUtils.isEmpty(userCardToken.getId())) {
            LogUtil.writeMessage("该卡["+userCardToken.getCardNo()+"]已经成功开通");
            resp.getWriter().print("ok");
            return;
        }
        //
        openCardLogMapper.updateLogStatus(openCardLog.getId(), "callback", "银联回调成功");
        //
        String cardNo = "";
        if(null != openCardLog) {
            cardNo = openCardLog.getCardNo();
        }
        //重要！验证签名前不要修改reqParam中的键值对的内容，否则会验签不过
        if (!AcpService.validate(valideData, encoding)) {
            LogUtil.writeLog("验证签名结果[失败].");
            //验签失败，需解决验签问题
            openCardLogMapper.updateLogStatus(openCardLog.getId(), "failed", "验证签名失败");

        } else {
            LogUtil.writeLog("验证签名结果[成功].");
            openCardLogMapper.updateLogStatus(openCardLog.getId(), "success", "验证签名成功");
            //
            String respCode = valideData.get("respCode");
            //判断respCode=00、A6后，对涉及资金类的交易，请再发起查询接口查询，确定交易成功后更新数据库。
            if(("00").equals(respCode)){
                //成功=======
                userCardToken = new UserCardToken();
                userCardToken.setId(StrUtils.uuid());
                //
                userCardToken.setCardNo(cardNo);
                userCardToken.setCreateTime(new Date());
                userCardToken.setUserUUID(openCardLog.getUserUUID());
                //bankCardToken.setCardType(cardType);
                String customerInfo = valideData.get("customerInfo");
                if(null!=customerInfo){
                    Map<String,String>  cm = AcpService.parseCustomerInfo(customerInfo, "UTF-8");//customerInfoMap明文: {phoneNo=13552535506}
                    //=======
                    //bankCardToken.setExpired(cm.get("expired"));
                    userCardToken.setPhoneNo(cm.get("phoneNo"));
                }
                String accNo = valideData.get("accNo");
                //如果配置了敏感信息加密证书，可以用以下方法解密，如果不加密可以不需要解密
                if(null!=accNo){
                    accNo = AcpService.decryptData(accNo, "UTF-8");
                    LogUtil.writeLog("accNo明文: "+ accNo);//accNo明文: 0018
                    //=======
                    userCardToken.setCardLastNo(accNo);
                }


                String tokenPayData = valideData.get("tokenPayData");
                if(null!=tokenPayData){
                    Map<String,String> tokenPayDataMap = SDKUtil.parseQString(tokenPayData.substring(1, tokenPayData.length() - 1));
                    String token = tokenPayDataMap.get("token");//这样取
                    LogUtil.writeLog("tokenPayDataMap明文: " + tokenPayDataMap);
                    //=======
                    userCardToken.setTokenLevel(tokenPayDataMap.get("tokenLevel"));
                    userCardToken.setTokenEnd(tokenPayDataMap.get("tokenEnd"));
                    userCardToken.setTokenType(tokenPayDataMap.get("tokenType"));
                    userCardToken.setTokenBegin(tokenPayDataMap.get("tokenBegin"));
                    userCardToken.setToken(tokenPayDataMap.get("token"));
                    userCardToken.setTrId(tokenPayDataMap.get("trId"));
                }

                //
                AlipayCardInfoResponse alipayCardInfoResponse = getCardInfo(openCardLog.getCardNo());
                if(null != alipayCardInfoResponse) {
                    userCardToken.setBankShortName(alipayCardInfoResponse.getBank());
                    userCardToken.setIconUrl("https://apimg.alipay.com/combo.png?d=cashier&t="+alipayCardInfoResponse.getBank());
                }
                //绑定用户和该卡的关联关系
                userCardTokenMapper.addBankCardToken(userCardToken);
                //
                openCardLogMapper.updateLogStatus(openCardLog.getId(), "success", "开通成功");
                //返回给银联服务器http 200状态码
                resp.getWriter().print("ok");
            }else{
                //其他应答码为失败请排查原因或做失败处理
                //TODO
                openCardLogMapper.updateLogStatus(openCardLog.getId(), "failed", "银联开通失败(" + respCode+")，"+ UnionpayErrorCodeEnum.getMsg(respCode));
            }
        }
        LogUtil.writeLog("BackRcvResponse接收后台通知结束");
    }


    private AlipayCardInfoResponse getCardInfo(String cardNo) {
        String reqUrl = "https://ccdcapi.alipay.com/validateAndCacheCardInfo.json?_input_charset-utf-8&cardNo="+cardNo+"&cardBinCheck=true";
//        Map<String, String> rspData = AcpService.post(null,reqUrl,"UTF-8");
        //发送后台请求数据
        HttpClient hc = new HttpClient(reqUrl, 30000, 30000);
        try {
            int status = hc.send(null, "utf-8");
            if (200 == status) {
                String resultString = hc.getResult();
                if (null != resultString && !"".equals(resultString)) {
                    // 将返回结果转换为Java 对象
                    AlipayCardInfoResponse response = JSONUtils.parseObject(resultString, AlipayCardInfoResponse.class);
                    return response;
                }
            }else{
                LogUtil.writeLog("返回http状态码["+status+"]，请检查请求报文或者请求地址是否正确");
            }
        } catch (Exception e) {
            LogUtil.writeErrorLog(e.getMessage(), e);
        }
        return null;
    }


    @Override
    public String openCardFrontFrontRcvResponse(HttpServletRequest req, HttpServletResponse response) throws IOException {
        LogUtil.writeLog("FrontRcvResponse前台接收报文返回开始");

        String encoding = req.getParameter(SDKConstants.param_encoding);
        LogUtil.writeLog("返回报文中encoding=[" + encoding + "]");
        Map<String, String> respParam = UnionpayUtils.getAllRequestParam(req);

        // 打印请求报文
        LogUtil.printRequestLog(respParam);

        Map<String, String> valideData = null;
        StringBuffer page = new StringBuffer();
        if (null != respParam && !respParam.isEmpty()) {
            Iterator<Map.Entry<String, String>> it = respParam.entrySet()
                    .iterator();
            valideData = new HashMap<String, String>(respParam.size());
            while (it.hasNext()) {
                Map.Entry<String, String> e = it.next();
                String key = (String) e.getKey();
                String value = (String) e.getValue();

                page.append("<tr><td width=\"30%\" align=\"right\">" + key
                        + "(" + key + ")</td><td>" + value + "</td></tr>");
                valideData.put(key, value);
            }
        }
        if (!AcpService.validate(valideData, encoding)) {
            page.append("<tr><td width=\"30%\" align=\"right\">验证签名结果</td><td>失败</td></tr>");
            LogUtil.writeLog("验证签名结果[失败].");
        } else {
            page.append("<tr><td width=\"30%\" align=\"right\">验证签名结果</td><td>成功</td></tr>");
            LogUtil.writeLog("验证签名结果[成功].");
            System.out.println(valideData.get("orderId")); //其他字段也可用类似方式获取

            //如果是获取token号的交易，解析tokenPayData域
            String customerInfo = valideData.get("customerInfo");
            if(null!=customerInfo){
                Map<String,String>  customerInfoMap = AcpService.parseCustomerInfo(customerInfo, "UTF-8");
                page.append("customerInfo明文: "+customerInfoMap + "<br>");
            }

//			String accNo = valideData.get("accNo");
//			//如果返回的卡号是密文那么，可以用下边方法解密,如果不是密文可以不用解密
//			if(null!= accNo){
//				accNo = AcpService.decryptData(accNo, "UTF-8");
//				page.append("<br>accNo明文: "+accNo);
//			}

            String tokenPayData = valideData.get("tokenPayData");
            if(null!=tokenPayData){
                Map<String,String> tokenPayDataMap = SDKUtil.parseQString(tokenPayData.substring(1, tokenPayData.length() - 1));
                String token = tokenPayDataMap.get("token");//这样取
                page.append("tokenPayDataMap明文: " + tokenPayDataMap + "<br>");
            }

            String respCode = valideData.get("respCode");
            //判断respCode=00、A6后，对涉及资金类的交易，请再发起查询接口查询，确定交易成功后更新数据库。
        }
        //
        LogUtil.writeLog("FrontRcvResponse前台接收报文返回结束");
        //
        return page.toString();
    }


































    //私有方法========================================================================
    private Reply openCardBackTokenPost(String userUUID, int cardType, String cardNo, String orderId, String phoneNo, String cvn2, String expired, String smsCode) throws IOException {
        //
        Map<String, String> contentData = new HashMap<String, String>();
        //
        /***银联全渠道系统，产品参数，除了encoding自行选择外其他不需修改***/
        contentData.put("version", SDKConfig.getConfig().getVersion());                   //版本号
        contentData.put("encoding", "UTF-8");            //字符集编码 可以使用UTF-8,GBK两种方式
        contentData.put("signMethod", SDKConfig.getConfig().getSignMethod()); //签名方法
        contentData.put("txnType", "79");                              //交易类型 11-代收
        contentData.put("txnSubType", "00");                           //交易子类型 00-默认开通
        contentData.put("bizType", "000902");                          //业务类型 token支付
        contentData.put("channelType", "07");                          //渠道类型07-PC


        /***商户接入参数***/
        contentData.put("merId", SDKConfig.getConfig().getMerId());    //商户号码（本商户号码仅做为测试调通交易使用，该商户号配置了需要对敏感信息加密）测试时请改成自己申请的商户号，【自己注册的测试777开头的商户号不支持代收产品】
        contentData.put("accessType", "0");                            //接入类型，商户接入固定填0，不需修改
        String outOrderId = UnionpayUtils.getOutOrderId();
        contentData.put("orderId", outOrderId);             			   //商户订单号，8-40位数字字母，不能含“-”或“_”，可以自行定制规则
        String txnTime = DateUtils.getCurrentDateTime();
        contentData.put("txnTime", txnTime);    //订单发送时间，格式为YYYYMMDDhhmmss，必须取当前时间，否则会报txnTime无效
        contentData.put("accType", "01");                              //账号类型

        //测试环境固定trId=62000000001&tokenType=01，生产环境由业务分配。测试环境因为所有商户都使用同一个trId，所以同一个卡获取的token号都相同，任一人发起更新token或者解除token请求都会导致原token号失效，所以之前成功、突然出现3900002报错时请先尝试重新开通一下。
        String trId = SDKConfig.getConfig().getTrId();
        String tokenPayDataStr = "{trId="+trId+"&tokenType=01}";
        contentData.put("tokenPayData", tokenPayDataStr);

        //贷记卡 必送：卡号、手机号、CVN2、有效期；验证码看业务配置（默认不要短信验证码）。
        //借记卡 必送：卡号、手机号；选送：证件类型+证件号、姓名；验证码看业务配置（默认不要短信验证码）。
        //此测试商户号777290058110097 后台开通业务只支持 贷记卡
        Map<String,String> customerInfoMap = new HashMap<String,String>();
        //customerInfoMap.put("certifTp", "01");						//证件类型
        //customerInfoMap.put("certifId", "341126197709218366");		//证件号码
        //customerInfoMap.put("customerNm", "全渠道");					//姓名
        customerInfoMap.put("phoneNo", phoneNo);			        //手机号
        //customerInfoMap.put("pin", "123456");						    //密码【这里如果送密码 商户号必须配置 ”商户允许采集密码“】
        customerInfoMap.put("cvn2", cvn2);           			        //卡背面的cvn2三位数字
        customerInfoMap.put("expired", expired);  				        //有效期 年在前月在后
        customerInfoMap.put("smsCode", smsCode);  				        //短信验证码

        ////////////如果商户号开通了【商户对敏感信息加密】的权限那么需要对 accNo，pin和phoneNo，cvn2，expired加密（如果这些上送的话），对敏感信息加密使用：
        String accNo1 = AcpService.encryptData(cardNo, "UTF-8");  //这里测试的时候使用的是测试卡号，正式环境请使用真实卡号
        contentData.put("accNo", accNo1);
        contentData.put("encryptCertId",AcpService.getEncryptCertId());       //加密证书的certId，配置在acp_sdk.properties文件 acpsdk.encryptCert.path属性下
        String customerInfoStr = AcpService.getCustomerInfoWithEncrypt(customerInfoMap,accNo1,"UTF-8");
        //////////


        /////////如果商户号未开通【商户对敏感信息加密】权限那么不需对敏感信息加密使用：
        //contentData.put("accNo", "6221558812340000");            		//这里测试的时候使用的是测试卡号，正式环境请使用真实卡号
        //String customerInfoStr = AcpService.getCustomerInfo(customerInfoMap,"6221558812340000","UTF-8"_UTF8);
        ////////

        contentData.put("customerInfo", customerInfoStr);

        // 请求方保留域，
        // 透传字段，查询、通知、对账文件中均会原样出现，如有需要请启用并修改自己希望透传的数据。
        // 出现部分特殊字符时可能影响解析，请按下面建议的方式填写：
        // 1. 如果能确定内容不会出现&={}[]"'等符号时，可以直接填写数据，建议的方法如下。
//		contentData.put("reqReserved", "透传信息1|透传信息2|透传信息3");
        // 2. 内容可能出现&={}[]"'符号时：
        // 1) 如果需要对账文件里能显示，可将字符替换成全角＆＝｛｝【】“‘字符（自己写代码，此处不演示）；
        // 2) 如果对账文件没有显示要求，可做一下base64（如下）。
        //    注意控制数据长度，实际传输的数据长度不能超过1024位。
        //    查询、通知等接口解析时使用new String(Base64.decodeBase64(reqReserved), "UTF-8");解base64后再对数据做后续解析。
//		contentData.put("reqReserved", Base64.encodeBase64String("任意格式的信息都可以".toString().getBytes("UTF-8")));

        OpenCardLog openCardLog = new OpenCardLog();
        String id = UUID.randomUUID().toString();
        openCardLog.setId(id);
        openCardLog.setUserUUID(userUUID);
        openCardLog.setOrderId(orderId);
        openCardLog.setCardType(cardType);
        openCardLog.setCvn2(cvn2);
        openCardLog.setExpired(expired);
        openCardLog.setCardNo(cardNo);
        openCardLog.setPhoneNo(phoneNo);
        openCardLog.setCreateTime(new Date());
        openCardLog.setSmsCode(smsCode);
        openCardLog.setVersion(SDKConfig.getConfig().getVersion());
        openCardLog.setMerId(SDKConfig.getConfig().getMerId());
        openCardLog.setOutOrderId(outOrderId);
        openCardLog.setTxnTime(txnTime);
        openCardLog.setAccType("01");
        openCardLog.setTokenPayData(tokenPayDataStr);
        openCardLog.setStatus("create");
        openCardLog.setMemo("");
        //
        openCardLogMapper.addOneItem(openCardLog);


        /**对请求参数进行签名并发送http post请求，接收同步应答报文**/
        Map<String, String> reqData = AcpService.sign(contentData,"UTF-8");			  //报文中certId,signature的值是在signData方法中获取并自动赋值的，只要证书配置正确即可。
        String requestBackUrl = SDKConfig.getConfig().getBackRequestUrl();   					  //交易请求url从配置文件读取对应属性文件acp_sdk.properties中的 acpsdk.backTransUrl
        Map<String, String> rspData = AcpService.post(reqData,requestBackUrl,"UTF-8");  //发送请求报文并接受同步应答（默认连接超时时间30秒，读取返回结果超时时间30秒）;这里调用signData之后，调用submitUrl之前不能对submitFromData中的键值对做任何修改，如果修改会导致验签不通过


        /**对应答码的处理，请根据您的业务逻辑来编写程序,以下应答码处理逻辑仅供参考------------->**/
        //应答码规范参考open.unionpay.com帮助中心 下载  产品接口规范  《平台接入接口规范-第5部分-附录》

        if(!rspData.isEmpty()){
            if(AcpService.validate(rspData, "UTF-8")){
                LogUtil.writeLog("验证签名成功");
                String respCode = rspData.get("respCode") ;
                if(("00").equals(respCode)){
                    //成功=======
                    UserCardToken userCardToken = new UserCardToken();
                    userCardToken.setId(StrUtils.uuid());
                    //
                    userCardToken.setCardNo(cardNo);
                    userCardToken.setCardType(cardType);
                    //
                    String customerInfo = rspData.get("customerInfo");
                    if(null!=customerInfo){
                        Map<String,String>  cm = AcpService.parseCustomerInfo(customerInfo, "UTF-8");
                        //=======
                        userCardToken.setExpired(cm.get("expired"));
                        userCardToken.setPhoneNo(cm.get("phoneNo"));
                    }
                    //如果是配置了敏感信息加密，如果需要获取卡号的明文，可以按以下方法解密卡号
                    String an = rspData.get("accNo");
                    if(null!=an){
                        an = AcpService.decryptData(an, "UTF-8");
                        //=======
                        userCardToken.setCardLastNo(an);
                    }

                    String tokenPayData = rspData.get("tokenPayData");
                    if(null!=tokenPayData){
                        Map<String,String> tokenPayDataMap = SDKUtil.parseQString(tokenPayData.substring(1, tokenPayData.length() - 1));
                        //=======
                        userCardToken.setTokenLevel(tokenPayDataMap.get("tokenLevel"));
                        userCardToken.setTokenEnd(tokenPayDataMap.get("tokenEnd"));
                        userCardToken.setTokenType(tokenPayDataMap.get("tokenType"));
                        userCardToken.setTokenBegin(tokenPayDataMap.get("tokenBegin"));
                        userCardToken.setToken(tokenPayDataMap.get("token"));
                        userCardToken.setTrId(tokenPayDataMap.get("trId"));
                    }
                    //
                    openCardLogMapper.updateLogStatus(id, "success", "开通成功");
                    //
                    return Mutual.success(userCardToken);
                }else{
                    //其他应答码为失败请排查原因或做失败处理
                    //TODO
                    openCardLogMapper.updateLogStatus(id, "failed", "银联开通失败(" + respCode+")，"+ UnionpayErrorCodeEnum.getMsg(respCode));
                    return Mutual.unionpayError(respCode);
                }
            }else{
                LogUtil.writeErrorLog("验证签名失败");
                //TODO 检查验证签名失败的原因
                openCardLogMapper.updateLogStatus(id, "failed", "验证签名失败");
                return Mutual.serverError("验证签名失败");
            }
        }else{
            //未返回正确的http状态
            LogUtil.writeErrorLog("未获取到返回报文或返回http状态码非200");
            openCardLogMapper.updateLogStatus(id, "failed", "未获取到返回报文或返回http状态码非200");
            return Mutual.serverError("未获取到返回报文或返回http状态码非200");
        }
    }
}
